package gbase8s

import (
	"database/sql"
	"fmt"
	"net/url"
	"reflect"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/callbacks"
	"gorm.io/gorm/clause"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/migrator"
	"gorm.io/gorm/schema"

	_ "gitee.com/GBase8s/go-gci"
)

const (
	DriverName = "gbase8s"
)

type Config struct {
	DriverName   string
	DSN          string
	Conn         gorm.ConnPool // *sql.DB
	DBVer        string
	GormLogTrace bool

	// whether VARCHAR type size is character length, defaulting to byte length
	varcharSizeIsCharLength bool
	DefaultStringSize       uint
}

func ExtractLogTrace(dsnString string) (string, bool, error) {
	// Parse the DSN string as a URL
	u, err := url.Parse(dsnString)
	if err != nil {
		// Return an error if the DSN string is not a valid URL
		return "", false, err
	}

	// Get the query parameters from the URL
	queryParams := u.Query()

	// Extract the value of "GormLogTrace" from the query parameters
	logtracValue := queryParams.Get("GormLogTrace")

	// Determine the LogTracFlag based on the extracted value
	LogTracFlag := (logtracValue == "1")

	// Remove "GormLogTrace" from the query parameters
	queryParams.Del("GormLogTrace")

	// Update the URL with the modified query parameters
	u.RawQuery = queryParams.Encode()

	// Return the modified DSN string and the LogTracFlag
	return u.String(), LogTracFlag, nil
}

func CheckSQLModeAndDelimIdent(dsnString string) (string, error) {
	// Parse the DSN string as a URL
	u, err := url.Parse(dsnString)
	if err != nil {
		// Return an error if the DSN string is not a valid URL
		return "", err
	}

	// Get the query parameters from the URL
	queryParams := u.Query()

	// Check if "sqlmode" is present, if not, set it to "oracle"
	if queryParams.Get("sqlmode") == "" {
		queryParams.Set("sqlmode", "oracle")
	}

	// Check if "delimident" is present, if not, set it to "1"
	if queryParams.Get("delimident") == "" {
		queryParams.Set("delimident", "1")
	}

	// Update the URL with the modified query parameters
	u.RawQuery = queryParams.Encode()

	// Return the modified DSN string
	return u.String(), nil
}

func Open(dsn string) gorm.Dialector {
	dsn, _ = CheckSQLModeAndDelimIdent(dsn)
	dsn, logTraceValue, _ := ExtractLogTrace(dsn)

	return &Dialector{Config: &Config{DSN: dsn, GormLogTrace: logTraceValue}}
}

type Dialector struct {
	*Config
}

func (d Dialector) Name() string {
	return DriverName
}

func (d Dialector) Initialize(db *gorm.DB) (err error) {
	if d.DriverName == "" {
		d.DriverName = DriverName
	}

	if d.Conn != nil {
		db.ConnPool = d.Conn
	} else {
		conn, err := sql.Open(d.DriverName, d.DSN)
		if err != nil {
			return err
		}
		db.ConnPool = conn
	}

	callbackConfig := &callbacks.Config{
		CreateClauses: []string{"INSERT", "VALUES"},
		UpdateClauses: []string{"UPDATE", "SET", "WHERE"},
		DeleteClauses: []string{"DELETE", "FROM", "WHERE"},
	}
	callbacks.RegisterDefaultCallbacks(db, callbackConfig)

	if err = db.Callback().Create().Replace("gorm:create", Create); err != nil {
		return
	}

	return
}

func (d Dialector) Migrator(db *gorm.DB) gorm.Migrator {
	return Migrator{migrator.Migrator{Config: migrator.Config{
		DB:                          db,
		Dialector:                   d,
		CreateIndexAfterCreateTable: true,
	}}}
}

func (d Dialector) DataTypeOf(field *schema.Field) string {
	log.Debug("DataTypeOf", String("field", field.Name), String("dataType", string(field.DataType)), Int("size", field.Size), Stack("stack"))
	delete(field.TagSettings, "RESTRICT")

	var sqlType string
	switch field.DataType {
	case schema.Bool:
		sqlType = "NUMBER(1)"
	case schema.Int, schema.Uint:
		sqlType = "BIGINT"
		if field.Size > 0 && field.Size <= 8 {
			sqlType = "SMALLINT"
		}

		if field.AutoIncrement {
			sqlType = "BIGSERIAL"
		}
	case schema.Float:
		sqlType = "FLOAT"
	case schema.String, "VARCHAR2":
		size := field.Size
		defaultSize := d.DefaultStringSize

		if size == 0 {
			if defaultSize > 0 {
				size = int(defaultSize)
			} else {
				hasIndex := field.TagSettings["INDEX"] != "" || field.TagSettings["UNIQUE"] != ""
				// TEXT, GEOMETRY or JSON column can't have a default value
				if field.PrimaryKey || field.HasDefaultValue || hasIndex {
					size = 4000 // utf8mb4
				}
			}
		}

		if size > 0 && size <= 32765 {
			// 默认情况下 VARCHAR2 可以指定一个不超过 4000 的正整数作为字节长度
			if d.varcharSizeIsCharLength {
				if size*3 > 32765 {
					sqlType = "CLOB"
				} else {
					sqlType = fmt.Sprintf("VARCHAR2(%d CHAR)", size) // 字符长度（size * 3）
				}
			} else {
				sqlType = fmt.Sprintf("VARCHAR2(%d)", size)
			}
		} else {
			sqlType = "VARCHAR2(4000)"
		}
	case schema.Time:
		sqlType = "DATETIME YEAR TO FRACTION"
	case schema.Bytes:
		sqlType = "BLOB"
	default:
		sqlType = string(field.DataType)

		if strings.EqualFold(sqlType, "text") {
			sqlType = "CLOB2"
		}

		if sqlType == "" {
			panic(fmt.Sprintf("invalid sql type %s (%s) for oracle", field.FieldType.Name(), field.FieldType.String()))
		}
	}

	return sqlType
}

func (d Dialector) DefaultValueOf(field *schema.Field) clause.Expression {
	return clause.Expr{SQL: "DEFAULT"}
}

func (d Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement, v interface{}) {
	_ = writer.WriteByte('?')
}

func (d Dialector) QuoteTo(writer clause.Writer, str string) {
	if str != "" {
		var (
			underQuoted, selfQuoted bool
			continuousBacktick      int8
			shiftDelimiter          int8
		)

		for _, v := range []byte(str) {
			switch v {
			case '"':
				continuousBacktick++
				if continuousBacktick == 2 {
					_, _ = writer.WriteString(`""`)
					continuousBacktick = 0
				}
			case '.':
				if continuousBacktick > 0 || !selfQuoted {
					shiftDelimiter = 0
					underQuoted = false
					continuousBacktick = 0
					_ = writer.WriteByte('"')
				}
				_ = writer.WriteByte(v)
				continue
			default:
				if shiftDelimiter-continuousBacktick <= 0 && !underQuoted {
					_ = writer.WriteByte('"')
					underQuoted = true
					if selfQuoted = continuousBacktick > 0; selfQuoted {
						continuousBacktick -= 1
					}
				}

				for ; continuousBacktick > 0; continuousBacktick -= 1 {
					_, _ = writer.WriteString(`""`)
				}

				_ = writer.WriteByte(v)
			}
			shiftDelimiter++
		}

		if continuousBacktick > 0 && !selfQuoted {
			_, _ = writer.WriteString(`""`)
		}
		_ = writer.WriteByte('"')
	} else {
		_, _ = writer.WriteString(str)
	}
}

func (d Dialector) Explain(sql string, vars ...interface{}) string {
	if len(vars) == 1 {
		switch vars[0].(type) {
		case bool:
			if vars[0].(bool) {
				vars[0] = 1
			} else {
				vars[0] = 0
			}
		}
	}
	return logger.ExplainSQL(sql, nil, `'`, vars...)
}

func (d Dialector) DummyTableName() string {
	return "DUAL"
}

func getTimeValue(t time.Time) interface{} {
	if t.IsZero() {
		return sql.NullTime{}
	}
	return t
}
func convertCustomType(val interface{}) interface{} {
	rv := reflect.ValueOf(val)
	ri := rv.Interface()
	typeName := reflect.TypeOf(ri).Name()
	if reflect.TypeOf(val).Kind() == reflect.Ptr {
		if rv.IsNil() {
			typeName = rv.Type().Elem().Name()
		} else {
			for rv.Kind() == reflect.Ptr {
				rv = rv.Elem()
			}
			ri = rv.Interface()
			typeName = reflect.TypeOf(ri).Name()
		}
	}
	if typeName == "DeletedAt" {
		// gorm.DeletedAt
		if rv.IsZero() {
			val = sql.NullTime{}
		} else {
			val = getTimeValue(ri.(gorm.DeletedAt).Time)
		}
	} else if m := rv.MethodByName("Time"); m.IsValid() && m.Type().NumIn() == 0 {
		// custom time type
		for _, result := range m.Call([]reflect.Value{}) {
			if reflect.TypeOf(result.Interface()).Name() == "Time" {
				val = getTimeValue(result.Interface().(time.Time))
			}
		}
	}
	return val
}

func ptrDereference(obj interface{}) (value interface{}) {
	if obj == nil {
		return obj
	}
	if t := reflect.TypeOf(obj); t.Kind() != reflect.Ptr {
		return obj
	}

	v := reflect.ValueOf(obj)
	for v.Kind() == reflect.Ptr && !v.IsNil() {
		v = v.Elem()
	}
	if !v.IsValid() || v.Kind() == reflect.Ptr && v.IsNil() {
		return obj
	}
	value = v.Interface()
	return
}
